# 一、开始
Vue的编译原理相对响应式来说,讨论的比较少,但深入理解编译原理有助于分析Vue的全貌。
Vue的编译流程大致如下,主要经历了parse
、optimize
、generate
三个阶段:
parse
,对template
进行解析,生成ASToptimize
,对AST进行优化,标注静态节点generate
,根据AST生成render
函数
# 二、Vue版本
Vue有两个版本:
vue.js
: 完整版本,包含了模板编译的能力;vue.runtime.js
: 运行时版本,不提供模板编译能力,需要通过vue-loader
进行提前编译。
什么时候需要编译器,什么时候不需要编译器呢?
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
直接写render
函数不需要编译器,写template
模版就需要编译器。render
函数不够直观,平时我们都是写teamplate
模版。
- 如果用了
vue-loader
,template
会在构建时被编译,我们可以使用vue.runtime.min.js
; - 如果是在浏览器中直接通过
script
标签引入 Vue,需要使用vue.min.js
,运行的时候编译模板。
# 三、编译入口
本次分析的是Vue的v2.6.10
版本。
Vue的编译入口比较绕,主要是因为其考虑了参数和方法的复用。
在我们实例化Vue的时候,会调用$mount (opens new window)方法。
在带编译的Vue版本的入口文件src/platforms/web/entry-runtime-with-compiler.js
中,看一下$mount
方法:
// src/platforms/web/entry-runtime-with-compiler.js
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
}
}
return mount.call(this, el, hydrating)
}
src/platforms/web/compiler/index.js
中:
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
src/compiler/index
中:
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
src/compiler/create-compiler.js
中:
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
src/compiler/to-function.js
中:
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// compile
const compiled = compile(template, options)
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
return (cache[key] = res)
}
}
从上面可以看出,Vue的$mount
判断如果没有render
方法,会根据template
调用compileToFunctions
生成render
,然后调用之前mount
方法。
看一下调用链:
compileToFunctions
由createCompiler
生成,而createCompiler
又是createCompilerCreator
的返回结果,createCompilerCreator
接受一个baesCompile
参数,其类型为函数,这个baesCompile
其实就是编译的主函数。
# 四、编译流程
# 1. parse
parse
方法主要引用了parseHTML
进行解析,然后返回了AST根节点。
AST元素节点总共有3种类型,type
为1表示是普通元素,2表示是表达式,3表示是纯文本
看下下面这段template
:
<div id="app">
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
</div>
parse
过后的AST为:
{
type: 1,
tag: 'div',
attrsList: [{
name: 'id',
value: 'app',
start: 5,
end: 13,
}],
attrsMap: {
id: 'app',
},
children: [
// ...
],
rawAttrsMap: {
id: {
name: 'id',
value: 'app',
start: 5,
end: 13,
},
},
start: 0,
end: 175,
plain: false,
attrs: [{
name: 'id',
value: 'app',
start: 5,
end: 13,
}],
static: false,
staticRoot: false,
}
parseHTML
接收两个参数,template
和options
,options.start
、options.end
代表的是解析开始标签和结束标签的回调函数。
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
warn = options.warn || baseWarn
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start (tag, attrs, unary, start) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.start = start
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
if (!root) {
root = element
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
end (tag, start, end) {
const element = stack[stack.length - 1]
if (!inPre) {
// remove trailing whitespace node
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
element.children.pop()
}
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars (text: string, start: number, end: number) {
if (!currentParent) {
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment (text: string, start, end) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
})
return root
parseHTML
是根据正则从开始便签,维护AST,其关键是对正则的解析。
这里有个网站 (opens new window)可以对正则可视化。
# 2. optimize
export function optimize (root, options) {
if (!root) return
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
optimize
主要是调用markStatic
标记静态节点,和markStaticRoots
标记静态根节点。
这些标记出的静态节点在非首次patch
阶段,生成DOM的时候,是不会变的,从而提高渲染性能。
# 3. generate
generate
主要是调用genElement
递归生成code
,然后返回render
函数和staticRenderFns
。
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
上面的例子生成的render
函数为:
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
},
[(isShow) ? _c('ul', {
staticClass: "list",
class: bindCls
},
_l((data),
function(item, index) {
return _c('li', {
on: {
"click": function($event) {
return clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
}), 0) : _e()])
}
with(this) (opens new window)的意思是下面的语句块的所有未声明属性,都来自于with
后面的对象,这里是this
。也就是避免了写this._c
、this._l
、this._v
等等。
with语法的本质是将某对象添加到作用域链的顶部,如果在statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。
# 五、相关问题
vue-loader中有对template的编译,vue的runtime+compiler版本也有对template的编译,二者有何联系和区别呢?
vue-loader
中的compiler (opens new window)是由vue-template-compiler
引入的,这个库的源码在vue/packages (opens new window)中。
在vue/scripts/config.js (opens new window)中可以看出vue-template-compiler
和vue
是一起发布的。
vue-template-compiler
的入口文件是packages/vue-template-compiler/index.js (opens new window),其会比较vue-template-compiler
和vue
版本是否相同,如果不同会抛出错误,然后导出./build.js
中的内容。
build.js
是打包产物,其入口在vue/scripts/config.js (opens new window)中可以看到,是web/entry-compiler.js
。
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
}
web/entry-compiler.js (opens new window)内容如下,可以看到它其实和vue
共用了compiler
。
// vue/src/platforms/web/entry-compiler.js
export { parseComponent } from 'sfc/parser'
export { compile, compileToFunctions } from './compiler/index'
export { ssrCompile, ssrCompileToFunctions } from './server/compiler'
export { generateCodeFrame } from 'compiler/codeframe'
// vue/src/sfc/parser.js
import { parseHTML } from 'compiler/parser/html-parser'
export function parseComponent(content, options) {
function start() { /**/ }
function end() { /**/ }
function warn() { /**/ }
parseHTML(content, {
warn,
start,
end,
outputSourceRange: options.outputSourceRange
})
}
所以vue-loader
和vue
编译逻辑是基本相同的。
# 六、总结
本文简单介绍了下Vue的编译流程,对细节感兴趣的可以查看源码和阅读下方的相关资料。
Vue的编译因其特殊性,并没有借助babel
或acorn
,在阅读它的过程中,不禁感叹作者基本功多深厚,前端也可以如此精彩。